baubs git
Commit 1820bcde7774534f784e838a8a7266c30d20e044
Parents : 3ca5edf
Author : Jan-Henrik Bruhn <jan-henrik.bruhn@offis.de>
Date : 2025-12-20T23:13:09+01:00
feature: Migrate ConfirmDialog and BluetoothDevicePicker to shadcn dialogs
- Migrated ConfirmDialog to use shadcn AlertDialog component
- Replaced custom modal structure with AlertDialogContent, AlertDialogHeader, AlertDialogFooter
- Used AlertDialogAction and AlertDialogCancel for buttons
- Maintained danger/warning variant support with border-top styling
- Simplified code by removing manual escape key handling (built into AlertDialog)
- Migrated BluetoothDevicePicker to use shadcn Dialog component
- Replaced custom modal structure with DialogContent, DialogHeader, DialogFooter
- Used shadcn Button components with outline variant for device selection
- Maintained scanning state UI with loading spinner in DialogDescription
- Simplified code by removing manual escape key handling (built into Dialog)
- Disabled close button for better UX during device selection
Both migrations maintain the same external API and functionality while significantly reducing code complexity.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changes
2 files changed, 104 insertions(+), 168 deletions(-)
Diff
diff --git a/src/components/BluetoothDevicePicker.tsx b/src/components/BluetoothDevicePicker.tsx
index b828d03..c89c5e8 100644
--- a/src/components/BluetoothDevicePicker.tsx
+++ b/src/components/BluetoothDevicePicker.tsx
@@ -1,5 +1,14 @@
import { useEffect, useState, useCallback } from "react";
import type { BluetoothDevice } from "../types/electron";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
export function BluetoothDevicePicker() {
const [devices, setDevices] = useState<BluetoothDevice[]>([]);
@@ -40,111 +49,71 @@ export function BluetoothDevicePicker() {
setIsScanning(false);
}, []);
- // Handle escape key
- const handleEscape = useCallback(
- (e: KeyboardEvent) => {
- if (e.key === "Escape") {
- handleCancel();
- }
- },
- [handleCancel],
- );
-
- useEffect(() => {
- if (isOpen) {
- document.addEventListener("keydown", handleEscape);
- return () => document.removeEventListener("keydown", handleEscape);
- }
- }, [isOpen, handleEscape]);
-
- if (!isOpen) return null;
-
return (
- <div
- className="fixed inset-0 bg-black/50 dark:bg-black/70 flex items-center justify-center z-[1000]"
- onClick={handleCancel}
- >
- <div
- className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl max-w-lg w-[90%] m-4 border-t-4 border-primary-600 dark:border-primary-500"
- onClick={(e) => e.stopPropagation()}
- role="dialog"
- aria-labelledby="bluetooth-picker-title"
- aria-describedby="bluetooth-picker-message"
+ <Dialog open={isOpen} onOpenChange={(open) => !open && handleCancel()}>
+ <DialogContent
+ className="border-t-4 border-primary-600 dark:border-primary-500"
+ showCloseButton={false}
>
- <div className="p-6 border-b border-gray-300 dark:border-gray-600">
- <h3
- id="bluetooth-picker-title"
- className="m-0 text-base lg:text-lg font-semibold dark:text-white"
- >
- Select Bluetooth Device
- </h3>
- </div>
- <div className="p-6">
- {isScanning && devices.length === 0 ? (
- <div className="flex items-center gap-3 text-gray-700 dark:text-gray-300">
- <svg
- className="animate-spin h-5 w-5 text-primary-600 dark:text-primary-400"
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- >
- <circle
- className="opacity-25"
- cx="12"
- cy="12"
- r="10"
- stroke="currentColor"
- strokeWidth="4"
- ></circle>
- <path
- className="opacity-75"
- fill="currentColor"
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
- ></path>
- </svg>
- <span id="bluetooth-picker-message">
- Scanning for Bluetooth devices...
- </span>
- </div>
- ) : (
- <>
- <p
- id="bluetooth-picker-message"
- className="mb-4 leading-relaxed text-gray-900 dark:text-gray-100"
- >
- {devices.length} device{devices.length !== 1 ? "s" : ""} found.
- Select a device to connect:
- </p>
- <div className="space-y-2">
- {devices.map((device) => (
- <button
- key={device.deviceId}
- onClick={() => handleSelectDevice(device.deviceId)}
- className="w-full px-4 py-3 bg-gray-100 dark:bg-gray-700 text-left rounded-lg font-medium text-sm hover:bg-primary-100 dark:hover:bg-primary-900 hover:text-primary-900 dark:hover:text-primary-100 active:bg-primary-200 dark:active:bg-primary-800 transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-300 dark:focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
- aria-label={`Connect to ${device.deviceName}`}
- >
- <div className="font-semibold text-gray-900 dark:text-white">
- {device.deviceName}
- </div>
- <div className="text-xs text-gray-600 dark:text-gray-400 mt-1">
- {device.deviceId}
- </div>
- </button>
- ))}
+ <DialogHeader>
+ <DialogTitle>Select Bluetooth Device</DialogTitle>
+ <DialogDescription>
+ {isScanning && devices.length === 0 ? (
+ <div className="flex items-center gap-3 py-2">
+ <svg
+ className="animate-spin h-5 w-5 text-primary-600 dark:text-primary-400"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ >
+ <circle
+ className="opacity-25"
+ cx="12"
+ cy="12"
+ r="10"
+ stroke="currentColor"
+ strokeWidth="4"
+ />
+ <path
+ className="opacity-75"
+ fill="currentColor"
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
+ />
+ </svg>
+ <span>Scanning for Bluetooth devices...</span>
</div>
- </>
- )}
- </div>
- <div className="p-4 px-6 flex gap-3 justify-end border-t border-gray-300 dark:border-gray-600">
- <button
- onClick={handleCancel}
- className="px-6 py-2.5 bg-gray-600 dark:bg-gray-700 text-white rounded-lg font-semibold text-sm hover:bg-gray-700 dark:hover:bg-gray-600 active:bg-gray-800 dark:active:bg-gray-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-gray-300 dark:focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
- aria-label="Cancel device selection"
- >
+ ) : (
+ `${devices.length} device${devices.length !== 1 ? "s" : ""} found. Select a device to connect:`
+ )}
+ </DialogDescription>
+ </DialogHeader>
+
+ {!isScanning && devices.length > 0 && (
+ <div className="space-y-2">
+ {devices.map((device) => (
+ <Button
+ key={device.deviceId}
+ onClick={() => handleSelectDevice(device.deviceId)}
+ variant="outline"
+ className="w-full h-auto px-4 py-3 justify-start"
+ >
+ <div className="text-left">
+ <div className="font-semibold">{device.deviceName}</div>
+ <div className="text-xs text-muted-foreground mt-1">
+ {device.deviceId}
+ </div>
+ </div>
+ </Button>
+ ))}
+ </div>
+ )}
+
+ <DialogFooter>
+ <Button variant="outline" onClick={handleCancel}>
Cancel
- </button>
- </div>
- </div>
- </div>
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
);
}
diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx
index d984cdc..02498a2 100644
--- a/src/components/ConfirmDialog.tsx
+++ b/src/components/ConfirmDialog.tsx
@@ -1,4 +1,14 @@
-import { useEffect, useCallback } from "react";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
+import { cn } from "@/lib/utils";
interface ConfirmDialogProps {
isOpen: boolean;
@@ -21,75 +31,32 @@ export function ConfirmDialog({
onCancel,
variant = "warning",
}: ConfirmDialogProps) {
- // Handle escape key
- const handleEscape = useCallback(
- (e: KeyboardEvent) => {
- if (e.key === "Escape") {
- onCancel();
- }
- },
- [onCancel],
- );
-
- useEffect(() => {
- if (isOpen) {
- document.addEventListener("keydown", handleEscape);
- return () => document.removeEventListener("keydown", handleEscape);
- }
- }, [isOpen, handleEscape]);
-
- if (!isOpen) return null;
-
return (
- <div
- className="fixed inset-0 bg-black/50 dark:bg-black/70 flex items-center justify-center z-[1000]"
- onClick={onCancel}
- >
- <div
- className={`bg-white dark:bg-gray-800 rounded-lg shadow-2xl max-w-lg w-[90%] m-4 ${variant === "danger" ? "border-t-4 border-danger-600 dark:border-danger-500" : "border-t-4 border-warning-500 dark:border-warning-600"}`}
- onClick={(e) => e.stopPropagation()}
- role="dialog"
- aria-labelledby="dialog-title"
- aria-describedby="dialog-message"
+ <AlertDialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>
+ <AlertDialogContent
+ className={cn(
+ variant === "danger"
+ ? "border-t-4 border-danger-600 dark:border-danger-500"
+ : "border-t-4 border-warning-500 dark:border-warning-600",
+ )}
>
- <div className="p-6 border-b border-gray-300 dark:border-gray-600">
- <h3
- id="dialog-title"
- className="m-0 text-base lg:text-lg font-semibold dark:text-white"
- >
- {title}
- </h3>
- </div>
- <div className="p-6">
- <p
- id="dialog-message"
- className="m-0 leading-relaxed text-gray-900 dark:text-gray-100"
- >
- {message}
- </p>
- </div>
- <div className="p-4 px-6 flex gap-3 justify-end border-t border-gray-300 dark:border-gray-600">
- <button
- onClick={onCancel}
- className="px-6 py-2.5 bg-gray-600 dark:bg-gray-700 text-white rounded-lg font-semibold text-sm hover:bg-gray-700 dark:hover:bg-gray-600 active:bg-gray-800 dark:active:bg-gray-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-gray-300 dark:focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
- autoFocus
- aria-label="Cancel action"
- >
- {cancelText}
- </button>
- <button
+ <AlertDialogHeader>
+ <AlertDialogTitle>{title}</AlertDialogTitle>
+ <AlertDialogDescription>{message}</AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel onClick={onCancel}>{cancelText}</AlertDialogCancel>
+ <AlertDialogAction
onClick={onConfirm}
- className={
- variant === "danger"
- ? "px-6 py-2.5 bg-danger-600 dark:bg-danger-700 text-white rounded-lg font-semibold text-sm hover:bg-danger-700 dark:hover:bg-danger-600 active:bg-danger-800 dark:active:bg-danger-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-danger-300 dark:focus:ring-danger-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
- : "px-6 py-2.5 bg-primary-600 dark:bg-primary-700 text-white rounded-lg font-semibold text-sm hover:bg-primary-700 dark:hover:bg-primary-600 active:bg-primary-800 dark:active:bg-primary-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-300 dark:focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
- }
- aria-label={`Confirm: ${confirmText}`}
+ className={cn(
+ variant === "danger" &&
+ "bg-danger-600 hover:bg-danger-700 dark:bg-danger-700 dark:hover:bg-danger-600",
+ )}
>
{confirmText}
- </button>
- </div>
- </div>
- </div>
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
);
}
Served by rngit 1.3.3 - Generated in 0.04s